﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Security;

using Microsoft.Coding4Fun.TweetCraft.Twitter;
using Microsoft.Coding4Fun.TweetCraft.Wow;

namespace Microsoft.Coding4Fun.TweetCraft
{
    /// <summary>
    /// Implements TweetCraft application logic.
    /// </summary>
    public sealed class TweetCraft : ITweetCraft, IErrorHandler
    {
        private const string ScreenshotsRelativePath = "Screenshots";
        private const string PicturesRelativePath = "Pictures";
        private const string AddOnName = "TweetCraft";

        // Keys for values sent and received through the channel
        private const string TweetCraftStatusKey = "tweetCraftStatus";
        private const string OutgoingTweetsKey = "outgoingTweets";
        private const string UserPicturesKey = "userPictures";
        private const string FirstLoginKey = "firstLogin";
        private const string RepliesKey = "replies";
        private const string TweetsKey = "tweets";

        private readonly IWorldOfWarcraft worldOfWarcraft;

        private readonly ITwitterClient twitterClient;
        private readonly ITwitterBackgroundService twitterBackgroundService;
        private readonly ITwitterDispatcherService twitterDispatcherService;
        private readonly ITwitterUserPictureService twitterUserPictureService;

        private readonly ISavedVariablesChannelFactory savedVariablesChannelFactory;
        private ISavedVariablesChannel savedVariablesChannel;

        private string twitterToken;
        private SecureString twitterTokenSecret;
        private string[] pendingRequestToken;

        private bool isRunning;

        /// <summary>
        /// Initializes a new instance of the <see cref="TweetCraft"/> class.
        /// </summary>
        /// <param name="twitterClient">The Twitter service.</param>
        /// <param name="worldOfWarcraft">The World of Warcraft service.</param>
        /// <param name="savedVariablesChannelFactory">The channel factory.</param>
        /// <param name="twitterBackgroundService">The background service.</param>
        /// <param name="twitterUserPictureService">The user picture service.</param>
        /// <param name="twitterDispatcherService">The dispatcher service.</param>
        public TweetCraft(ITwitterClient twitterClient, IWorldOfWarcraft worldOfWarcraft, ISavedVariablesChannelFactory savedVariablesChannelFactory, ITwitterBackgroundService twitterBackgroundService, ITwitterUserPictureService twitterUserPictureService, ITwitterDispatcherService twitterDispatcherService)
        {
            if (twitterClient == null)
                throw new ArgumentNullException("twitterClient");
            if (worldOfWarcraft == null)
                throw new ArgumentNullException("worldOfWarcraft");
            if (savedVariablesChannelFactory == null)
                throw new ArgumentNullException("savedVariablesChannelFactory");
            if (twitterBackgroundService == null)
                throw new ArgumentNullException("twitterBackgroundService");
            if (twitterUserPictureService == null)
                throw new ArgumentNullException("twitterUserPictureService");
            if (twitterDispatcherService == null)
                throw new ArgumentNullException("twitterDispatcherService");

            this.twitterClient = twitterClient;
            this.worldOfWarcraft = worldOfWarcraft;
            this.savedVariablesChannelFactory = savedVariablesChannelFactory;
            this.twitterBackgroundService = twitterBackgroundService;
            this.twitterUserPictureService = twitterUserPictureService;
            this.twitterDispatcherService = twitterDispatcherService;

            // Advise services to report errors
            twitterBackgroundService.SubscribeError(this);
            twitterDispatcherService.SubscribeError(this);

            // Initialize services
            this.InitializeServices();

            // Initialize defaults (implicitly initializes the channel)
            this.InitializeDefaults();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TweetCraft"/> class.
        /// </summary>
        /// <param name="twitterClient">The Twitter service.</param>
        /// <param name="worldOfWarcraft">The World of Warcraft service.</param>
        public TweetCraft(ITwitterClient twitterClient, IWorldOfWarcraft worldOfWarcraft)
            : this(twitterClient, worldOfWarcraft, new SavedVariablesChannelFactory(worldOfWarcraft), new TwitterBackgroundService(twitterClient), new TwitterUserPictureService<WebDownloadClient, WorldOfWarcraftTargaConverter>(), new TwitterDispatcherService(twitterClient))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TweetCraft"/> class.
        /// </summary>
        /// <param name="twitterClient">The Twitter service.</param>
        public TweetCraft(ITwitterClient twitterClient)
            : this(twitterClient, new WorldOfWarcraft())
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TweetCraft"/> class.
        /// </summary>
        public TweetCraft()
            : this(new Twitter.TwitterClient())
        {

        }

        /// <summary>
        /// Gets a value indicating whether TweetCraft is running.
        /// </summary>
        public bool IsRunning
        {
            get { return isRunning; }
        }

        /// <summary>
        /// Gets or sets the Twitter token to use.
        /// </summary>
        public string TwitterToken
        {
            get { return twitterToken; }
            set
            {
                twitterToken =
                twitterClient.Token = value;
            }
        }

        /// <summary>
        /// Gets or sets the Twitter password to use.
        /// </summary>
        public SecureString TwitterTokenSecret
        {
            get { return twitterTokenSecret; }
            set
            {
                twitterTokenSecret =
                twitterClient.TokenSecret = value;
            }
        }

        /// <summary>
        /// Gets or sets the Twitter client status.
        /// </summary>
        public TwitterClientStatus TwitterStatus
        {
            get { return twitterClient.Status; }
        }

        /// <summary>
        /// Raised when the Twitter status changes.
        /// </summary>
        public event EventHandler TwitterStatusChanged
        {
            add { twitterClient.StatusChanged += value; }
            remove { twitterClient.StatusChanged -= value; }
        }

        /// <summary>
        /// Gets or sets the name of the World of Warcraft account to use.
        /// </summary>
        public string WowAccount
        {
            get { return savedVariablesChannelFactory.AccountName; }
            set
            {
                if (savedVariablesChannelFactory.AccountName != value)
                {
                    savedVariablesChannelFactory.AccountName = value;
                    InitializeChannel();
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether screenshots taken in World of Warcraft should be AutoTweeted or not.
        /// </summary>
        public bool AutoTweetScreenshots
        {
            get { return twitterDispatcherService.AutoTweetPictures; }
            set { twitterDispatcherService.AutoTweetPictures = value; }
        }

        private void InitializeServices()
        {
            Trace.TraceInformation("Initializing services...");
            Trace.Indent();

            // Set the addon name used for channels
            savedVariablesChannelFactory.AddOnName = AddOnName;

            // Set the picture directory under the AddOn's directory
            twitterUserPictureService.UserPictureDirectory = worldOfWarcraft.GetAddOnPath(AddOnName).Combine(PicturesRelativePath);

            // Set the picture directory to monitor to the Screenshots directory
            twitterDispatcherService.PictureDirectory = worldOfWarcraft.InstallPath.Combine(ScreenshotsRelativePath);

            // Subscribe to service events
            twitterBackgroundService.TweetsChanged += OnTweetsChanged;
            twitterDispatcherService.Tweeted += OnTweeted;

            Trace.Unindent();
            Trace.TraceInformation("Services initialized.");
        }

        private void InitializeDefaults()
        {
            // Initialize defaults
            if (worldOfWarcraft.IsDetected && worldOfWarcraft.AreAccountsAvailable)
                this.WowAccount = worldOfWarcraft.GetAccountNames().First();
        }

        private void InitializeChannel()
        {
            using (TraceScope trace = TraceScope.Information("Initializing communication channel..."))
            {
                if (savedVariablesChannel != null)
                {
                    savedVariablesChannel.UnsubscribeError(this);
                    savedVariablesChannel.ChannelUpdated -= OnChannelUpdated;
                    savedVariablesChannel.Flushed -= OnFlushed;

                    savedVariablesChannel.Dispose();
                    savedVariablesChannel = null;

                    Trace.TraceInformation("Disposed existing channel.");
                }

                // Check whether a channel can be created
                if (savedVariablesChannelFactory.CanCreateChannel())
                {
                    using (TraceScope channelTrace = TraceScope.Information("Creating communication channel..."))
                    {
                        // Create a new channel
                        savedVariablesChannel = savedVariablesChannelFactory.CreateChannel();

                        // Subscribe to events
                        savedVariablesChannel.SubscribeError(this);
                        savedVariablesChannel.ChannelUpdated += OnChannelUpdated;
                        savedVariablesChannel.Flushed += OnFlushed;

                        channelTrace.Complete("Created new communication channel.");
                    }
                }

                trace.Complete("Initialized communication channel.");
            }

            // If a new channel was initialized and World of Warcraft is not running, process values
            if (savedVariablesChannel != null && !worldOfWarcraft.IsRunning)
            {
                // Process channel values as it might already contain queued tweets
                this.ProcessValues();
            }
        }

        /// <summary>
        /// Gets a value indicating whether the settings are complete.
        /// </summary>
        public bool IsComplete
        {
            get
            {
                return !String.IsNullOrEmpty(this.TwitterToken) &&
                       this.TwitterTokenSecret.Length > 0;
            }
        }

        /// <summary>
        /// Runs TweetCraft and starts background services.
        /// </summary>
        public void Run()
        {
            if (isRunning)
                throw new InvalidOperationException(Strings.TweetCraftAlreadyRunning);
            if (!worldOfWarcraft.IsDetected)
                throw new InvalidOperationException(Strings.InvalidOperationWorldOfWarcraftNotDetected);

            // Enable polling and explicitly poll when TweetCraft is run
            twitterBackgroundService.PollingEnabled = true;
            twitterBackgroundService.PollAsync();

            isRunning = true;

            Trace.TraceInformation("TweetCraft is running.");
        }

        /// <summary>
        /// Stops TweetCraft and stops background services.
        /// </summary>
        public void Shutdown()
        {
            if (!isRunning)
                throw new InvalidOperationException(Strings.TweetCraftNotRunning);

            twitterBackgroundService.PollingEnabled = false;
            isRunning = false;

            Trace.TraceInformation("TweetCraft stopped.");
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (this.isRunning)
                this.Shutdown();

            if (twitterBackgroundService is IDisposable)
                ((IDisposable)twitterBackgroundService).Dispose();
            if (twitterDispatcherService is IDisposable)
                ((IDisposable)twitterDispatcherService).Dispose();

            if (twitterClient is IDisposable)
                ((IDisposable)twitterClient).Dispose();

            Trace.TraceInformation("TweetCraft disposed.");
        }

        /// <summary>
        /// Raised when a tweet has been sent.
        /// </summary>
        /// <seealso cref="AutoTweetScreenshots"/>
        public event EventHandler<TweetEventArgs> Tweeted;

        /// <summary>
        /// Authorizes a user account via OAuth.
        /// </summary>
        public void AuthorizeUser()
        {
            Trace.TraceInformation("User is being sent to Twitter for authorization.");
            pendingRequestToken = twitterClient.GetRequestToken();
            var authorizationUrl = "https://api.twitter.com/oauth/authorize?oauth_token=" + pendingRequestToken[0];
            Process.Start(authorizationUrl);
        }

        public string[] GetAccessToken(string verifier)
        {
            Trace.TraceInformation("User is exchanging OAuth PIN for an access token.");

            if(pendingRequestToken == null)
            {
                return new[]{"?", "?"};
            }

            var token = twitterClient.GetAccessToken(pendingRequestToken, verifier);
            if(token != null && token.Length >= 2)
            {
                return token;
            }
            return null;
        }

        /// <summary>
        /// Raised when an error occurs.
        /// </summary>
        public event EventHandler<ErrorEventArgs> Error;

        void IErrorHandler.OnError(object sender, ErrorEventArgs e)
        {
            Trace.TraceError("An error has occurred: {0}", e.Exception.Message);

            if (Error != null)
                Error(this, e);
        }

        private void OnTweeted(object sender, TweetEventArgs e)
        {
            Trace.TraceInformation(String.Format(CultureInfo.CurrentCulture, "A tweet has been sent with the message  '{0}'.", e.Tweet.Message));

            // Raise Tweeted event
            if (Tweeted != null)
                Tweeted(this, e);
        }

        private void OnTweetsChanged(object sender, EventArgs e)
        {
            Trace.TraceInformation("The available tweets have changed.");

            // Check whether the channel is available
            if (savedVariablesChannel != null)
            {
                // Queue values into the channel
                this.QueueValues();

                // If World of Warcraft is not running, we can immediately flush
                if (!worldOfWarcraft.IsRunning)
                {
                    Trace.TraceInformation("World of Warcraft is not running, flushing channel...");

                    // Flush the channel, but keep values
                    savedVariablesChannel.Flush(true);
                }
            }
        }

        private void OnChannelUpdated(object sender, EventArgs e)
        {
            Trace.TraceInformation("The channel has been updated.");

            // Process the values in the channel
            ProcessValues();

            // Immediately trigger a poll afterwards
            twitterBackgroundService.PollAsync();
            
            // Remove FirstLogin value as we've just received communication from World of Warcraft
            savedVariablesChannel.RemoveValue(FirstLoginKey);
        }

        private void OnFlushed(object sender, EventArgs e)
        {
            Trace.TraceInformation("The channel was flushed.");

            // Queue values into the channel (for next flush)
            this.QueueValues();
        }

        private void QueueValues()
        {
            using (TraceScope trace = TraceScope.Information("Queueing values into channel..."))
            {
                // Queue tweets and replies into the channel
                savedVariablesChannel.QueueValue(TweetsKey, twitterBackgroundService.Tweets.ToArray());
                savedVariablesChannel.QueueValue(RepliesKey, twitterBackgroundService.Replies.ToArray());

                Trace.TraceInformation("Queued {0} tweets and {1} replies.", twitterBackgroundService.Tweets.Count, twitterBackgroundService.Replies.Count);

                // Get the user pictures for all distinct users in tweets and replies
                Dictionary<int, string> userPictures = this.GetUserPictures();

                // Queue the dictionary of user pictures
                savedVariablesChannel.QueueValue(UserPicturesKey, userPictures);

                // Complete the trace
                Trace.TraceInformation("{0} user pictures queued.", userPictures.Count);

                // Check whether World of Warcraft is running, if not, add FirstLogin = true
                if (!worldOfWarcraft.IsRunning)
                {
                    savedVariablesChannel.QueueValue(FirstLoginKey, true);
                    Trace.TraceInformation("Added flag for first login.");
                }

                trace.Complete("Values queued.");
            }
        }

        private void ProcessValues()
        {
            using (TraceScope trace = TraceScope.Information("Processing channel values..."))
            {
                // Check whether channel contains tweets to be sent
                if (savedVariablesChannel.IsAvailable(OutgoingTweetsKey))
                {
                    // Get the value as an array of tweets
                    Tweet[] outgoingTweets = savedVariablesChannel.ReadValue<Tweet[]>(OutgoingTweetsKey);

                    // Sort the outgoing tweets by their TimeSent as we receive them in random order
                    Array.Sort(outgoingTweets, (left, right) => left.DateSent.CompareTo(right.DateSent));

                    // Check whether outgoing tweets contain one or more tweets to be sent
                    if (outgoingTweets != null && outgoingTweets.Length > 0)
                    {
                        Trace.TraceInformation("Queueing {0} outgoing tweets...", outgoingTweets.Length);

                        // Queue outgoing tweets for sending in dispatcher service
                        twitterDispatcherService.QueueTweets(outgoingTweets);

                        // Update TweetCraft status message to reflect that the tweets were queued
                        savedVariablesChannel.QueueValue(TweetCraftStatusKey, String.Format(CultureInfo.CurrentCulture, Strings.TweetCraftSentTweetsStatusMessageFormat, outgoingTweets.Length));
                    }
                }
                else
                    Trace.TraceInformation("Channel does not contain any outgoing tweets.");

                trace.Complete("Channel values processed.");
            }
        }

        private Dictionary<int, string> GetUserPictures()
        {
            using (TraceScope trace = TraceScope.Information("Retrieving user pictures..."))
            {
                var userPictures = new Dictionary<int, string>();

                // Iterate through the distinct set of users and collect picture paths for them
                IEnumerable<User> users = twitterBackgroundService.Tweets.Concat(twitterBackgroundService.Replies)
                    .Select(tweet => tweet.User)
                    .Distinct();

                Trace.TraceInformation("{0} distinct user pictures available.", users.Count());

                foreach (User user in users)
                {
                    using (TraceScope userTrace = TraceScope.Information("Retrieving {0}'s user picture...", user.ScreenName))
                    {
                        string picturePath = twitterUserPictureService.GetUserPicturePath(user);
                        if (!String.IsNullOrEmpty(picturePath))
                        {
                            userPictures.Add(user.Id, picturePath.RelativeTo(worldOfWarcraft.InstallPath));
                            userTrace.Complete("User picture available at {0}", picturePath);
                        }
                        else
                            userTrace.Complete("Could not retrieve user picture.");
                    }
                }

                trace.Complete("User pictures retrieved.");

                return userPictures;
            }
        }
    }
}
